1   package org.apache.lucene.search;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  import java.io.IOException;
21  import java.util.Collections;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Random;
25  
26  import junit.framework.Assert;
27  
28  import org.apache.lucene.index.BinaryDocValues;
29  import org.apache.lucene.index.FieldInfo;
30  import org.apache.lucene.index.FieldInfos;
31  import org.apache.lucene.index.Fields;
32  import org.apache.lucene.index.IndexReader;
33  import org.apache.lucene.index.LeafReader;
34  import org.apache.lucene.index.LeafReaderContext;
35  import org.apache.lucene.index.MultiReader;
36  import org.apache.lucene.index.NumericDocValues;
37  import org.apache.lucene.index.SortedDocValues;
38  import org.apache.lucene.index.SortedNumericDocValues;
39  import org.apache.lucene.index.SortedSetDocValues;
40  import org.apache.lucene.index.StoredFieldVisitor;
41  import org.apache.lucene.index.Terms;
42  import org.apache.lucene.search.spans.SpanBoostQuery;
43  import org.apache.lucene.util.Bits;
44  import org.apache.lucene.util.LuceneTestCase;
45  
46  import static junit.framework.Assert.assertEquals;
47  import static junit.framework.Assert.assertFalse;
48  import static junit.framework.Assert.assertTrue;
49  
50  /**
51   * Utility class for sanity-checking queries.
52   */
53  public class QueryUtils {
54  
55    /** Check the types of things query objects should be able to do. */
56    public static void check(Query q) {
57      checkHashEquals(q);
58  
59      if (q instanceof FilteredQuery) {
60        // This is our best option to have coverage on filters since they are
61        // rarely searched on directly
62        // This hack can go away when FilteredQuery goes away too
63        FilteredQuery filtered = (FilteredQuery) q;
64        check(filtered.getQuery());
65        check(filtered.getFilter());
66      }
67  
68      try {
69        IndexSearcher dummySearcher = new IndexSearcher(new MultiReader());
70        Query clone = q.clone();
71        clone.setBoost((float) Math.PI);
72        Query rewritten = dummySearcher.rewrite(clone);
73        Assert.assertTrue("Query " + clone.getClass() + " does not propagate Query.rewrite call to super.rewrite",
74            rewritten instanceof BoostQuery || rewritten instanceof SpanBoostQuery);
75      } catch (IOException ioe) {
76        throw new AssertionError("Unexpected I/O error", ioe);
77      }
78    }
79  
80    /** check very basic hashCode and equals */
81    public static void checkHashEquals(Query q) {
82      checkEqual(q,q);
83  
84      Query q2 = q.clone();
85      checkEqual(q,q2);
86  
87      Query q3 = q.clone();
88      q3.setBoost(7.21792348f);
89      checkUnequal(q,q3);
90  
91      // test that a class check is done so that no exception is thrown
92      // in the implementation of equals()
93      Query whacky = new Query() {
94        @Override
95        public String toString(String field) {
96          return "My Whacky Query";
97        }
98      };
99      checkUnequal(q, whacky);
100     
101     // null test
102     assertFalse(q.equals(null));
103   }
104 
105   public static void checkEqual(Query q1, Query q2) {
106     assertEquals(q1, q2);
107     assertEquals(q1.hashCode(), q2.hashCode());
108   }
109 
110   public static void checkUnequal(Query q1, Query q2) {
111     assertFalse(q1 + " equal to " + q2, q1.equals(q2));
112     assertFalse(q2 + " equal to " + q1, q2.equals(q1));
113 
114     // possible this test can fail on a hash collision... if that
115     // happens, please change test to use a different example.
116     assertTrue(q1.hashCode() != q2.hashCode());
117   }
118   
119   /** deep check that explanations of a query 'score' correctly */
120   public static void checkExplanations (final Query q, final IndexSearcher s) throws IOException {
121     CheckHits.checkExplanations(q, null, s, true);
122   }
123   
124   /** 
125    * Various query sanity checks on a searcher, some checks are only done for
126    * instanceof IndexSearcher.
127    *
128    * @see #check(Query)
129    * @see #checkFirstSkipTo
130    * @see #checkSkipTo
131    * @see #checkExplanations
132    * @see #checkEqual
133    */
134   public static void check(Random random, Query q1, IndexSearcher s) {
135     check(random, q1, s, true);
136   }
137   public static void check(Random random, Query q1, IndexSearcher s, boolean wrap) {
138     try {
139       check(q1);
140       if (s!=null) {
141         checkFirstSkipTo(q1,s);
142         checkSkipTo(q1,s);
143         checkBulkScorerSkipTo(random, q1, s);
144         if (wrap) {
145           check(random, q1, wrapUnderlyingReader(random, s, -1), false);
146           check(random, q1, wrapUnderlyingReader(random, s,  0), false);
147           check(random, q1, wrapUnderlyingReader(random, s, +1), false);
148         }
149         checkExplanations(q1,s);
150       }
151     } catch (IOException e) {
152       throw new RuntimeException(e);
153     }
154   }
155   
156   /** This is a MultiReader that can be used for randomly wrapping other readers
157    * without creating FieldCache insanity.
158    * The trick is to use an opaque/fake cache key. */
159   public static class FCInvisibleMultiReader extends MultiReader {
160     private final Object cacheKey = new Object();
161   
162     public FCInvisibleMultiReader(IndexReader... readers) throws IOException {
163       super(readers);
164     }
165     
166     @Override
167     public Object getCoreCacheKey() {
168       return cacheKey;
169     }
170     
171     @Override
172     public Object getCombinedCoreAndDeletesKey() {
173       return cacheKey;
174     }
175   }
176 
177   /**
178    * Given an IndexSearcher, returns a new IndexSearcher whose IndexReader 
179    * is a MultiReader containing the Reader of the original IndexSearcher, 
180    * as well as several "empty" IndexReaders -- some of which will have 
181    * deleted documents in them.  This new IndexSearcher should 
182    * behave exactly the same as the original IndexSearcher.
183    * @param s the searcher to wrap
184    * @param edge if negative, s will be the first sub; if 0, s will be in the middle, if positive s will be the last sub
185    */
186   public static IndexSearcher wrapUnderlyingReader(Random random, final IndexSearcher s, final int edge) 
187     throws IOException {
188 
189     IndexReader r = s.getIndexReader();
190 
191     // we can't put deleted docs before the nested reader, because
192     // it will throw off the docIds
193     IndexReader[] readers = new IndexReader[] {
194       edge < 0 ? r : new MultiReader(),
195       new MultiReader(),
196       new FCInvisibleMultiReader(edge < 0 ? emptyReader(4) : new MultiReader(),
197           new MultiReader(),
198           0 == edge ? r : new MultiReader()),
199       0 < edge ? new MultiReader() : emptyReader(7),
200       new MultiReader(),
201       new FCInvisibleMultiReader(0 < edge ? new MultiReader() : emptyReader(5),
202           new MultiReader(),
203           0 < edge ? r : new MultiReader())
204     };
205 
206     IndexSearcher out = LuceneTestCase.newSearcher(new FCInvisibleMultiReader(readers));
207     out.setSimilarity(s.getSimilarity(true));
208     return out;
209   }
210   
211   private static IndexReader emptyReader(final int maxDoc) {
212     return new LeafReader() {
213 
214       @Override
215       public void addCoreClosedListener(CoreClosedListener listener) {}
216 
217       @Override
218       public void removeCoreClosedListener(CoreClosedListener listener) {}
219 
220       @Override
221       public Fields fields() throws IOException {
222         return new Fields() {
223           @Override
224           public Iterator<String> iterator() {
225             return Collections.<String>emptyList().iterator();
226           }
227 
228           @Override
229           public Terms terms(String field) throws IOException {
230             return null;
231           }
232 
233           @Override
234           public int size() {
235             return 0;
236           }
237         };
238       }
239 
240       @Override
241       public NumericDocValues getNumericDocValues(String field) throws IOException {
242         return null;
243       }
244 
245       @Override
246       public BinaryDocValues getBinaryDocValues(String field) throws IOException {
247         return null;
248       }
249 
250       @Override
251       public SortedDocValues getSortedDocValues(String field) throws IOException {
252         return null;
253       }
254 
255       @Override
256       public SortedNumericDocValues getSortedNumericDocValues(String field) throws IOException {
257         return null;
258       }
259 
260       @Override
261       public SortedSetDocValues getSortedSetDocValues(String field) throws IOException {
262         return null;
263       }
264 
265       @Override
266       public Bits getDocsWithField(String field) throws IOException {
267         return null;
268       }
269 
270       @Override
271       public NumericDocValues getNormValues(String field) throws IOException {
272         return null;
273       }
274 
275       @Override
276       public FieldInfos getFieldInfos() {
277         return new FieldInfos(new FieldInfo[0]);
278       }
279       
280       final Bits liveDocs = new Bits.MatchNoBits(maxDoc);
281       @Override
282       public Bits getLiveDocs() {
283         return liveDocs;
284       }
285 
286       @Override
287       public void checkIntegrity() throws IOException {}
288 
289       @Override
290       public Fields getTermVectors(int docID) throws IOException {
291         return null;
292       }
293 
294       @Override
295       public int numDocs() {
296         return 0;
297       }
298 
299       @Override
300       public int maxDoc() {
301         return maxDoc;
302       }
303 
304       @Override
305       public void document(int docID, StoredFieldVisitor visitor) throws IOException {}
306 
307       @Override
308       protected void doClose() throws IOException {}
309     };
310   }
311 
312   /** alternate scorer advance(),advance(),next(),next(),advance(),advance(), etc
313    * and ensure a hitcollector receives same docs and scores
314    */
315   public static void checkSkipTo(final Query q, final IndexSearcher s) throws IOException {
316     //System.out.println("Checking "+q);
317     final List<LeafReaderContext> readerContextArray = s.getTopReaderContext().leaves();
318 
319     final int skip_op = 0;
320     final int next_op = 1;
321     final int orders [][] = {
322         {next_op},
323         {skip_op},
324         {skip_op, next_op},
325         {next_op, skip_op},
326         {skip_op, skip_op, next_op, next_op},
327         {next_op, next_op, skip_op, skip_op},
328         {skip_op, skip_op, skip_op, next_op, next_op},
329     };
330     for (int k = 0; k < orders.length; k++) {
331 
332         final int order[] = orders[k];
333         // System.out.print("Order:");for (int i = 0; i < order.length; i++)
334         // System.out.print(order[i]==skip_op ? " skip()":" next()");
335         // System.out.println();
336         final int opidx[] = { 0 };
337         final int lastDoc[] = {-1};
338 
339         // FUTURE: ensure scorer.doc()==-1
340 
341         final float maxDiff = 1e-5f;
342         final LeafReader lastReader[] = {null};
343 
344         s.search(q, new SimpleCollector() {
345           private Scorer sc;
346           private Scorer scorer;
347           private int leafPtr;
348 
349           @Override
350           public void setScorer(Scorer scorer) {
351             this.sc = scorer;
352           }
353 
354           @Override
355           public void collect(int doc) throws IOException {
356             float score = sc.score();
357             lastDoc[0] = doc;
358             try {
359               if (scorer == null) {
360                 Weight w = s.createNormalizedWeight(q, true);
361                 LeafReaderContext context = readerContextArray.get(leafPtr);
362                 scorer = w.scorer(context);
363               }
364               
365               int op = order[(opidx[0]++) % order.length];
366               // System.out.println(op==skip_op ?
367               // "skip("+(sdoc[0]+1)+")":"next()");
368               boolean more = op == skip_op ? scorer.advance(scorer.docID() + 1) != DocIdSetIterator.NO_MORE_DOCS
369                   : scorer.nextDoc() != DocIdSetIterator.NO_MORE_DOCS;
370               int scorerDoc = scorer.docID();
371               float scorerScore = scorer.score();
372               float scorerScore2 = scorer.score();
373               float scoreDiff = Math.abs(score - scorerScore);
374               float scorerDiff = Math.abs(scorerScore2 - scorerScore);
375 
376               boolean success = false;
377               try {
378                 assertTrue(more);
379                 assertEquals("scorerDoc=" + scorerDoc + ",doc=" + doc, scorerDoc, doc);
380                 assertTrue("score=" + score + ", scorerScore=" + scorerScore, scoreDiff <= maxDiff);
381                 assertTrue("scorerScorer=" + scorerScore + ", scorerScore2=" + scorerScore2, scorerDiff <= maxDiff);
382                 success = true;
383               } finally {
384                 if (!success) {
385                   if (LuceneTestCase.VERBOSE) {
386                     StringBuilder sbord = new StringBuilder();
387                     for (int i = 0; i < order.length; i++) {
388                       sbord.append(order[i] == skip_op ? " skip()" : " next()");
389                     }
390                     System.out.println("ERROR matching docs:" + "\n\t"
391                         + (doc != scorerDoc ? "--> " : "") + "doc=" + doc + ", scorerDoc=" + scorerDoc
392                         + "\n\t" + (!more ? "--> " : "") + "tscorer.more=" + more
393                         + "\n\t" + (scoreDiff > maxDiff ? "--> " : "")
394                         + "scorerScore=" + scorerScore + " scoreDiff=" + scoreDiff
395                         + " maxDiff=" + maxDiff + "\n\t"
396                         + (scorerDiff > maxDiff ? "--> " : "") + "scorerScore2="
397                         + scorerScore2 + " scorerDiff=" + scorerDiff
398                         + "\n\thitCollector.doc=" + doc + " score=" + score
399                         + "\n\t Scorer=" + scorer + "\n\t Query=" + q + "  "
400                         + q.getClass().getName() + "\n\t Searcher=" + s
401                         + "\n\t Order=" + sbord + "\n\t Op="
402                         + (op == skip_op ? " skip()" : " next()"));
403                   }
404                 }
405               }
406             } catch (IOException e) {
407               throw new RuntimeException(e);
408             }
409           }
410 
411           @Override
412           public boolean needsScores() {
413             return true;
414           }
415 
416           @Override
417           protected void doSetNextReader(LeafReaderContext context) throws IOException {
418             // confirm that skipping beyond the last doc, on the
419             // previous reader, hits NO_MORE_DOCS
420             if (lastReader[0] != null) {
421               final LeafReader previousReader = lastReader[0];
422               IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader);
423               indexSearcher.setSimilarity(s.getSimilarity(true));
424               Weight w = indexSearcher.createNormalizedWeight(q, true);
425               LeafReaderContext ctx = (LeafReaderContext)indexSearcher.getTopReaderContext();
426               Scorer scorer = w.scorer(ctx);
427               if (scorer != null) {
428                 boolean more = false;
429                 final Bits liveDocs = context.reader().getLiveDocs();
430                 for (int d = scorer.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = scorer.nextDoc()) {
431                   if (liveDocs == null || liveDocs.get(d)) {
432                     more = true;
433                     break;
434                   }
435                 }
436                 Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more);
437               }
438               leafPtr++;
439             }
440             lastReader[0] = context.reader();
441             assert readerContextArray.get(leafPtr).reader() == context.reader();
442             this.scorer = null;
443             lastDoc[0] = -1;
444           }
445         });
446 
447         if (lastReader[0] != null) {
448           // confirm that skipping beyond the last doc, on the
449           // previous reader, hits NO_MORE_DOCS
450           final LeafReader previousReader = lastReader[0];
451           IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader, false);
452           indexSearcher.setSimilarity(s.getSimilarity(true));
453           Weight w = indexSearcher.createNormalizedWeight(q, true);
454           LeafReaderContext ctx = previousReader.getContext();
455           Scorer scorer = w.scorer(ctx);
456           if (scorer != null) {
457             boolean more = false;
458             final Bits liveDocs = lastReader[0].getLiveDocs();
459             for (int d = scorer.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = scorer.nextDoc()) {
460               if (liveDocs == null || liveDocs.get(d)) {
461                 more = true;
462                 break;
463               }
464             }
465             Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more);
466           }
467         }
468       }
469   }
470     
471   /** check that first skip on just created scorers always goes to the right doc */
472   public static void checkFirstSkipTo(final Query q, final IndexSearcher s) throws IOException {
473     //System.out.println("checkFirstSkipTo: "+q);
474     final float maxDiff = 1e-3f;
475     final int lastDoc[] = {-1};
476     final LeafReader lastReader[] = {null};
477     final List<LeafReaderContext> context = s.getTopReaderContext().leaves();
478     s.search(q,new SimpleCollector() {
479       private Scorer scorer;
480       private int leafPtr;
481       @Override
482       public void setScorer(Scorer scorer) {
483         this.scorer = scorer;
484       }
485       @Override
486       public void collect(int doc) throws IOException {
487         float score = scorer.score();
488         try {
489           long startMS = System.currentTimeMillis();
490           for (int i=lastDoc[0]+1; i<=doc; i++) {
491             Weight w = s.createNormalizedWeight(q, true);
492             Scorer scorer = w.scorer(context.get(leafPtr));
493             Assert.assertTrue("query collected "+doc+" but advance("+i+") says no more docs!",scorer.advance(i) != DocIdSetIterator.NO_MORE_DOCS);
494             Assert.assertEquals("query collected "+doc+" but advance("+i+") got to "+scorer.docID(),doc,scorer.docID());
495             float advanceScore = scorer.score();
496             Assert.assertEquals("unstable advance("+i+") score!",advanceScore,scorer.score(),maxDiff); 
497             Assert.assertEquals("query assigned doc "+doc+" a score of <"+score+"> but advance("+i+") has <"+advanceScore+">!",score,advanceScore,maxDiff);
498             
499             // Hurry things along if they are going slow (eg
500             // if you got SimpleText codec this will kick in):
501             if (i < doc && System.currentTimeMillis() - startMS > 5) {
502               i = doc-1;
503             }
504           }
505           lastDoc[0] = doc;
506         } catch (IOException e) {
507           throw new RuntimeException(e);
508         }
509       }
510       
511       @Override
512       public boolean needsScores() {
513         return true;
514       }
515 
516       @Override
517       protected void doSetNextReader(LeafReaderContext context) throws IOException {
518         // confirm that skipping beyond the last doc, on the
519         // previous reader, hits NO_MORE_DOCS
520         if (lastReader[0] != null) {
521           final LeafReader previousReader = lastReader[0];
522           IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader);
523           indexSearcher.setSimilarity(s.getSimilarity(true));
524           Weight w = indexSearcher.createNormalizedWeight(q, true);
525           Scorer scorer = w.scorer((LeafReaderContext)indexSearcher.getTopReaderContext());
526           if (scorer != null) {
527             boolean more = false;
528             final Bits liveDocs = context.reader().getLiveDocs();
529             for (int d = scorer.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = scorer.nextDoc()) {
530               if (liveDocs == null || liveDocs.get(d)) {
531                 more = true;
532                 break;
533               }
534             }
535             Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more);
536           }
537           leafPtr++;
538         }
539 
540         lastReader[0] = context.reader();
541         lastDoc[0] = -1;
542       }
543     });
544 
545     if (lastReader[0] != null) {
546       // confirm that skipping beyond the last doc, on the
547       // previous reader, hits NO_MORE_DOCS
548       final LeafReader previousReader = lastReader[0];
549       IndexSearcher indexSearcher = LuceneTestCase.newSearcher(previousReader);
550       indexSearcher.setSimilarity(s.getSimilarity(true));
551       Weight w = indexSearcher.createNormalizedWeight(q, true);
552       Scorer scorer = w.scorer((LeafReaderContext)indexSearcher.getTopReaderContext());
553       if (scorer != null) {
554         boolean more = false;
555         final Bits liveDocs = lastReader[0].getLiveDocs();
556         for (int d = scorer.advance(lastDoc[0] + 1); d != DocIdSetIterator.NO_MORE_DOCS; d = scorer.nextDoc()) {
557           if (liveDocs == null || liveDocs.get(d)) {
558             more = true;
559             break;
560           }
561         }
562         Assert.assertFalse("query's last doc was "+ lastDoc[0] +" but advance("+(lastDoc[0]+1)+") got to "+scorer.docID(),more);
563       }
564     }
565   }
566 
567   /** Check that the scorer and bulk scorer advance consistently. */
568   public static void checkBulkScorerSkipTo(Random r, Query query, IndexSearcher searcher) throws IOException {
569     Weight weight = searcher.createNormalizedWeight(query, true);
570     for (LeafReaderContext context : searcher.getIndexReader().leaves()) {
571       final Scorer scorer = weight.scorer(context);
572       final BulkScorer bulkScorer = weight.bulkScorer(context);
573       if (scorer == null && bulkScorer == null) {
574         continue;
575       } else if (bulkScorer == null) {
576         // ensure scorer is exhausted (it just didnt return null)
577         assert scorer.nextDoc() == DocIdSetIterator.NO_MORE_DOCS;
578         continue;
579       }
580       int upTo = 0;
581       while (true) {
582         final int min = upTo + r.nextInt(5);
583         final int max = min + 1 + r.nextInt(r.nextBoolean() ? 10 : 5000);
584         if (scorer.docID() < min) {
585           scorer.advance(min);
586         }
587         final int next = bulkScorer.score(new LeafCollector() {
588           Scorer scorer2;
589           @Override
590           public void setScorer(Scorer scorer) throws IOException {
591             this.scorer2 = scorer;
592           }
593           @Override
594           public void collect(int doc) throws IOException {
595             assert doc >= min;
596             assert doc < max;
597             Assert.assertEquals(scorer.docID(), doc);
598             Assert.assertEquals(scorer.score(), scorer2.score(), 0.01f);
599             scorer.nextDoc();
600           }
601         }, null, min, max);
602         assert max <= next;
603         assert next <= scorer.docID();
604         upTo = max;
605 
606         if (scorer.docID() == DocIdSetIterator.NO_MORE_DOCS) {
607           bulkScorer.score(new LeafCollector() {
608             @Override
609             public void setScorer(Scorer scorer) throws IOException {}
610             
611             @Override
612             public void collect(int doc) throws IOException {
613               // no more matches
614               assert false;
615             }
616           }, null, upTo, DocIdSetIterator.NO_MORE_DOCS);
617           break;
618         }
619       }
620     }
621   }
622 }